Suivi à distance : le parent consulte la progression de son enfant depuis son propre appareil - #72
Merged
Merged
Conversation
… appareil Le multi-profils couvre la tablette familiale partagée, mais pas le cas où l'enfant pratique sur un appareil qui n'est pas celui du parent (ancien téléphone en WiFi seul). Le tableau de bord parent existait déjà, mais il vivait sur l'appareil de l'enfant. L'appareil de l'enfant publie après chaque séance un instantané de son profil gzippé puis chiffré côté client (AES-GCM), sous un code de 96 bits. L'appareil du parent scanne le QR une fois et relit ce dépôt. Même primitive que le transfert : la clé ne vit que dans le fragment d'URL, jamais transmise au serveur. Deux différences, qui justifient une table à part : le dépôt est durable et la lecture non consommante. Le profil suivi n'est jamais installé chez le parent — installé, il apparaîtrait dans « Qui joue ? » et une séance faite dessus par erreur divergerait de l'appareil de l'enfant. Un parent sans Tablito est pris en charge de bout en bout : le fragment #watch= saute la landing statique et l'app ouvre directement l'espace parent, jamais l'onboarding enfant. L'espace parent affiche les deux sources en parallèle (profil local + enfants suivis) via un sélecteur en tête, le corps de stats étant extrait en ParentStats pour un rendu identique des deux côtés. - supabase/watches.sql : table + 3 RPC SECURITY DEFINER, RLS sans policy - lib/watchStore (localStorage, eager) / lib/watch (réseau+crypto, dynamique) pour ne pas faire entrer le chiffrement dans le graphe de modules du boot - supabaseRpc() remonté dans lib/supabase, adopté par transfer.ts - scanner QR extrait en hook useQrScan, réutilisé par WelcomeScreen - confidentialité (fr/en) : nouvelle section, 4e exception déclarée ; specs §14 - « par défaut » ajouté aux affirmations « les données restent sur l'appareil »
…en code stable npm run lint (lancé par la CI) refusait deux choses dans ParentDashboard : l'écriture d'une ref pendant le render (react-hooks/refs) et un setState atteignable depuis un effet (react-hooks/set-state-in-effect). La relecture d'un suivi chaîne désormais sa promesse INLINE dans l'effet, forme que la règle accepte et déjà employée par NotificationSettings. L'état « chargement » n'est plus posé au changement de source : l'absence d'instantané pour la source affichée EST le chargement, donc il se dérive. « Actualiser » le pose explicitement, mais depuis un handler d'événement, où c'est légitime. La source sélectionnée repasse d'un objet à un code (string | null, null = progression locale) : c'est l'identité stable dont dépend l'effet, donc re-cliquer l'onglet courant ne relance plus rien — un objet recréé à chaque render annulait la relecture en vol sans la redémarrer.
Contributor
|
Preview supprimée (PR fermée). Les URLs ne sont plus accessibles. |
… appareil vierge Un parent sans Tablito qui scanne un QR expiré ou révoqué n'a ni profil local ni suivi mémorisé. L'échec d'appairage forçait bien l'écran 'parent', mais le garde de rendu exigeait l'un des deux — donc rien ne s'affichait, sans aucun chemin de récupération alors que l'espace parent permet précisément de réessayer. Le garde tient désormais compte de watchPairing, y compris quand il vaut 'error'. Test de non-régression sur un appareil totalement vierge.
…suiveur Le chemin « parent qui découvre Tablito par le QR de son enfant, puis se crée un profil pour lui-même » n'était vérifié que par morceaux (présence du bouton, sortie d'onboarding possible, appareil mixte seedé à la main). Ce test joue le parcours complet et vérifie que la création n'a pas chassé le suivi.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Le besoin
Le multi-profils (§13) couvre la tablette familiale partagée. Il ne couvre pas le cas, très courant, où l'enfant pratique sur un appareil qui n'est pas celui du parent — un ancien téléphone de la famille, en WiFi seul, gardé pour lui. Le tableau de bord parent existait déjà, mais il vivait sur l'appareil de l'enfant, donc le parent n'avait aucune visibilité.
Une alternative envisagée puis écartée : le recap par e-mail. Il aurait imposé de collecter une adresse, de faire transiter les stats de l'enfant en clair sur un serveur, et d'ajouter un expéditeur à configurer — pour une information moins riche et moins fraîche que ce que donne un miroir chiffré consultable à la demande.
L'approche
Deux rôles, non exclusifs sur un même appareil.
C'est la primitive du transfert (§7) : la clé voyage dans le fragment d'URL, jamais transmise au serveur, qui ne voit passer qu'un blob opaque. Deux différences justifient une table distincte de
transfers: le dépôt est durable (rafraîchi au lieu d'être consommé) et la lecture non consommante.Le profil suivi n'est jamais installé chez le parent : il ne vit qu'en mémoire. Installé, il apparaîtrait dans « Qui joue ? » et une séance faite dessus par erreur divergerait de l'appareil de l'enfant, que le dépôt suivant écraserait.
Un parent qui n'a pas encore Tablito
Pris en charge de bout en bout : le fragment
#watch=saute la landing statique, et l'app ouvre directement l'espace parent avec la progression de l'enfant. Aucun onboarding enfant (prénom, test de placement) ne lui est proposé — il n'est pas venu s'entraîner. Une porte de sortie « Créer un profil sur cet appareil » reste disponible s'il veut aussi le sien.Les deux sources en parallèle
Un sélecteur en tête de l'espace parent bascule entre le profil local (qui continue de vivre) et chaque enfant suivi. Il n'apparaît que s'il y a vraiment un choix. Le corps de stats est extrait en
ParentStatspour que la vue distante soit exactement le même rendu que la locale. Une barre de fraîcheur indique la date du dernier dépôt (« Synchronisé il y a 2 h ») : sans elle, un appareil éteint depuis une semaine afficherait des chiffres périmés sans le dire.Sécurité et confidentialité
Le code est une capacité permanente jusqu'à révocation. Garde-fous : 96 bits (inénumérable), clé jamais exposée au serveur (un dump de la base ne donne rien de lisible), « Ne plus partager » qui supprime le dépôt immédiatement, purge automatique après 6 mois sans rafraîchissement.
La page Confidentialité gagne une section (fr/en) : le vrai delta à déclarer n'est pas le chiffrement (identique au transfert) mais la durée. Les quatre endroits qui affirmaient « les données restent sur l'appareil » — dont la meta description, donc le SEO — gagnent « par défaut », ce qui les rend exacts (c'était déjà approximatif avec le transfert et le rappel quotidien).
Structure
supabase/watches.sql— table + 3 RPCSECURITY DEFINER, RLS activée sans policy, index surupdated_atlib/watchStore.ts(localStorage, eager) /lib/watch.ts(réseau + crypto, chargé à la demande) — la séparation évite que le chiffrement entre dans le graphe de modules du bootsupabaseRpc()remonté danslib/supabase.tset adopté partransfer.ts, qui en avait deux copies open-codéesuseQrScan, réutilisé parWelcomeScreen; QR rendu parQrCanvasVérifications
watch.test.ts: chiffrement, durabilité, révocation, non-installation ;remoteFollow.test.tsx: boot suiveur-only, masquage des sections locales, sélecteur de source, sortie d'onboarding)tsc -bclean, build prod OKwatchettransferrestent lazy, 81 modules eager contre 79 avant (les 2 ajoutés sontwatchStore, voulu, etuseQrScan, inévitable puisqueWelcomeScreenest eager)À faire avant déploiement
psql "$SUPABASE_DB_URL" -f supabase/watches.sqlsur l'instance — le projet n'a pas de système de migrations, la DDL s'applique à la main.Suite
La notification hebdomadaire au parent (« le recap de Zoé est prêt ») part dans une PR séparée : elle demande des colonnes en plus sur
push_subscriptionset de la logique cron, et sa version propre garde un corps de notif générique — le Service Worker ne peut pas lirelocalStorage, donc pas de déchiffrement de son côté.🤖 Generated with Claude Code